/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.plugin.maven.server.core;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

import org.eclipse.che.maven.data.MavenArtifact;
import org.eclipse.che.maven.data.MavenKey;
import org.eclipse.che.maven.data.MavenWorkspaceCache;
import org.eclipse.che.maven.server.MavenTerminal;
import org.eclipse.che.plugin.maven.server.MavenServerManager;
import org.eclipse.che.plugin.maven.server.MavenServerWrapper;
import org.eclipse.che.plugin.maven.server.MavenWrapperManager;
import org.eclipse.che.plugin.maven.server.core.project.MavenProject;
import org.eclipse.che.plugin.maven.server.core.project.MavenProjectModifications;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IWorkspace;

import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;

/**
 * Holds all maven projects in workspace
 *
 * @author Evgen Vidolob
 */
@Singleton
public class MavenProjectManager {

    private final MavenWorkspaceCache                   mavenWorkspaceCache;
    private final Map<MavenKey, MavenProject>           keyToProjectMap;
    private final Map<IProject, MavenProject>           projectToMavenProjectMap;
    private final Map<MavenProject, List<MavenProject>> parentToModulesMap;
    private final Map<MavenProject, MavenProject>       moduleToParentMap;

    private final List<MavenProjectListener> listeners = new CopyOnWriteArrayList<>();

    //project that does not have parent project in our workspace
    private final List<MavenProject> rootProjects;

    private final MavenWrapperManager   wrapperManager;
    private final MavenServerManager    serverManager;
    private final MavenTerminal         terminal;
    private final MavenProgressNotifier mavenNotifier;
    private final Provider<IWorkspace>  workspaceProvider;

    private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private final Lock                   readLock      = readWriteLock.readLock();
    private final Lock                   writeLock     = readWriteLock.writeLock();

    private final MavenProjectListener dispatcher;

    @Inject
    public MavenProjectManager(MavenWrapperManager wrapperManager,
                               MavenServerManager serverManager,
                               MavenTerminal terminal,
                               MavenProgressNotifier mavenNotifier,
                               EclipseWorkspaceProvider workspaceProvider) {
        this.wrapperManager = wrapperManager;
        this.serverManager = serverManager;
        this.terminal = terminal;
        this.mavenNotifier = mavenNotifier;
        this.workspaceProvider = workspaceProvider;
        mavenWorkspaceCache = new MavenWorkspaceCache();
        keyToProjectMap = new HashMap<>();
        projectToMavenProjectMap = new HashMap<>();
        parentToModulesMap = new HashMap<>();
        dispatcher = createListenersDispatcher();
        moduleToParentMap = new HashMap<>();
        rootProjects = new ArrayList<>();
    }

    private MavenProjectListener createListenersDispatcher() {
        return (MavenProjectListener)Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                                                            new Class[] {MavenProjectListener.class},
                                                            (proxy, method, args) -> {
                                                                for (MavenProjectListener listener : listeners) {
                                                                    method.invoke(listener, args);
                                                                }
                                                                return null;
                                                            });
    }

    public void addListener(MavenProjectListener listener) {
        listeners.add(listener);
    }

    public void removeListener(MavenProjectListener listener) {
        listeners.remove(listener);
    }

    public void resolveMavenProject(IProject project, MavenProject mavenProject) {
        MavenServerWrapper mavenServer = wrapperManager.getMavenServer(MavenWrapperManager.ServerType.RESOLVE);
        try {

            mavenNotifier.setText("Resolving project: " + mavenProject.getName());
            mavenServer.customize(copyWorkspaceCache(), terminal, mavenNotifier, false, true);
            MavenProjectModifications modifications = mavenProject.resolve(project, mavenServer, serverManager);
            dispatcher.projectResolved(mavenProject, modifications);

        } finally {
            wrapperManager.release(mavenServer);
        }

    }


    public void update(List<IProject> projects, boolean recursive) {
        if (projects.isEmpty()) {
            return;
        }
        mavenNotifier.start();
        UpdateState state = new UpdateState();
        Deque<MavenProject> stack = new LinkedList<>();
        for (IProject project : projects) {
            MavenProject mavenProject = findMavenProject(project);
            if (mavenProject != null) {
                internalUpdate(mavenProject, findParentProject(mavenProject), false, recursive, state, stack);
            } else {
                internalAddMavenProject(project, recursive, state, stack);
            }
        }
        mavenNotifier.stop();
        state.fireUpdate();

    }

    public MavenProject findParentProject(MavenProject mavenProject) {
        readLock.lock();
        try {
            return moduleToParentMap.get(mavenProject);
        } finally {
            readLock.unlock();
        }

    }

    private void internalAddMavenProject(IProject project, boolean recursive, UpdateState state, Deque<MavenProject> stack) {
        MavenProject mavenProject = new MavenProject(project, workspaceProvider.get());
        MavenProject potentialParent = null;
        for (MavenProject parent : getAllProjects()) {
            if (parent.containsAsModule(project.getFullPath())) {
                potentialParent = parent;
                break;
            }
        }
        internalUpdate(mavenProject, potentialParent, true, recursive, state, stack);
    }

    private void internalUpdate(MavenProject mavenProject, MavenProject parentProject, boolean isNew, boolean recursive, UpdateState state,
                                Deque<MavenProject> stack) {
        if (stack.contains(mavenProject)) {
            return; //recursion
        }

        stack.addFirst(mavenProject);

        mavenNotifier.setText("Reading pom: " + mavenProject.getPomPath());

        List<MavenProject> oldModules = findModules(mavenProject);
        Set<MavenProject> oldChilds = new HashSet<>();
        if (!isNew) {
            oldChilds.addAll(findChildProjects(mavenProject));
        }

        writeLock.lock();
        try {
            if (!isNew) {
                clearMavenKeyMap(mavenProject);
            }
        } finally {
            writeLock.unlock();
        }
        MavenProjectModifications modifications = new MavenProjectModifications();
        //re read maven project meta info from pom.xml
        modifications = modifications.addChanges(mavenProject.read(serverManager));

        writeLock.lock();
        try {
            fillMavenKeyMap(mavenProject);
            projectToMavenProjectMap.put(mavenProject.getProject(), mavenProject);
        } finally {
            writeLock.unlock();
        }

        if (isNew) {
            addToChild(parentProject, mavenProject);
        } else {
            updateChild(parentProject, mavenProject);
        }

//        if (hasparent) {
        state.addUpdate(mavenProject, modifications);
//        }

        List<IProject> modules = mavenProject.getModulesProjects();
        List<MavenProject> removedModules =
                oldModules.stream()
                          .filter(oldModule -> !modules.contains(oldModule.getProject()))
                          .collect(Collectors.toList());

        for (MavenProject removedModule : removedModules) {
            removeModule(mavenProject, removedModule);
            internalDelete(mavenProject, removedModule, state);
            oldChilds.removeAll(state.removedProjects);
        }

        for (IProject module : modules) {
            MavenProject project = findMavenProject(module);
            boolean isNewProject = project == null;
            if (isNewProject) {
                project = new MavenProject(module, workspaceProvider.get());
            } else {
                MavenProject parent = findParentProject(project);
                if (parent != null && parent != mavenProject) {
                    //TODO add log
                    continue;
                }
            }

            if (isNewProject || recursive) {
                internalUpdate(project, mavenProject, isNewProject, recursive, state, stack);
            } else {
                if (updateChild(mavenProject, project)) {
                    state.addUpdate(project, new MavenProjectModifications());
                }
            }
        }
        oldChilds.addAll(findChildProjects(mavenProject));

        for (MavenProject oldModule : oldChilds) {
            internalUpdate(oldModule, findParentProject(oldModule), false, false, state, stack);
        }

        stack.pop();
    }

    private void internalDelete(MavenProject parentProject, MavenProject removedModule, UpdateState state) {
        for (MavenProject project : findModules(removedModule)) {
            internalDelete(removedModule, project, state);
        }

        writeLock.lock();
        try {
            if (parentProject == null) {
                rootProjects.remove(removedModule);
            } else {
                removeModule(parentProject, removedModule);
            }

            projectToMavenProjectMap.remove(removedModule.getProject());
            clearMavenKeyMap(removedModule);
            moduleToParentMap.remove(removedModule);
            parentToModulesMap.remove(removedModule);
        } finally {
            writeLock.unlock();
        }

        state.remove(removedModule);

    }

    private boolean updateChild(MavenProject parentProject, MavenProject module) {
        MavenProject oldParent = findParentProject(module);
        if (oldParent == parentProject) {
            return false;
        }
        writeLock.lock();
        try {
            if (oldParent == null) {
                rootProjects.remove(module);
            } else {
                removeModule(oldParent, module);
            }

            if (parentProject == null) {
                rootProjects.add(module);
            } else {
                addModule(parentProject, module);
            }
        } finally {
            writeLock.unlock();
        }

        return false;
    }

    private void removeModule(MavenProject oldParent, MavenProject module) {
        writeLock.lock();
        try {
            List<MavenProject> modules = parentToModulesMap.get(oldParent);
            if (modules != null) {
                modules.remove(module);
                moduleToParentMap.remove(module);
            }
        } finally {
            writeLock.unlock();
        }
    }

    private void addToChild(MavenProject parentProject, MavenProject module) {
        writeLock.lock();
        try {
            if (parentProject == null) {
                rootProjects.add(module);
            } else {
                addModule(parentProject, module);
            }
        } finally {
            writeLock.unlock();
        }

    }

    private void addModule(MavenProject parentProject, MavenProject module) {
        writeLock.lock();
        try {
            List<MavenProject> modules = parentToModulesMap.get(parentProject);
            if (modules == null) {
                modules = new ArrayList<>();
                parentToModulesMap.put(parentProject, modules);
            }

            modules.add(module);
            moduleToParentMap.put(module, parentProject);
        } finally {
            writeLock.unlock();
        }
    }

    public List<MavenProject> getAllProjects() {
        readLock.lock();
        try {
            return new ArrayList<>(projectToMavenProjectMap.values());
        } finally {
            readLock.unlock();
        }
    }

    private void fillMavenKeyMap(MavenProject mavenProject) {
        MavenKey mavenKey = mavenProject.getMavenKey();
        mavenWorkspaceCache.put(mavenKey, mavenProject.getPomFile());
        keyToProjectMap.put(mavenKey, mavenProject);
    }

    private void clearMavenKeyMap(MavenProject mavenProject) {
        MavenKey mavenKey = mavenProject.getMavenKey();
        mavenWorkspaceCache.invalidate(mavenKey);
        keyToProjectMap.remove(mavenKey);
    }

    private List<MavenProject> findChildProjects(MavenProject mavenProject) {
        readLock.lock();
        try {
            MavenKey parentKey = mavenProject.getMavenKey();
            return projectToMavenProjectMap.values().stream()
                                           .filter(project -> mavenProject != project)
                                           .filter(project -> project.getParentKey().equals(parentKey))
                                           .collect(Collectors.toList());
        } finally {
            readLock.unlock();
        }
    }

    public List<MavenProject> findModules(MavenProject parent) {
        readLock.lock();
        try {
            List<MavenProject> modules = parentToModulesMap.get(parent);
            if (modules == null) {
                modules = Collections.emptyList();
            }
            return new ArrayList<>(modules);
        } finally {
            readLock.unlock();
        }
    }

    public MavenProject findMavenProject(IProject project) {
        readLock.lock();
        try {
            return projectToMavenProjectMap.get(project);
        } finally {
            readLock.unlock();
        }
    }

    public MavenWorkspaceCache copyWorkspaceCache() {
        readLock.lock();
        try {
            return mavenWorkspaceCache.copy();
        } finally {
            readLock.unlock();
        }
    }

    public List<MavenProject> findDependentProjects(List<MavenProject> projects) {
        readLock.lock();
        try {
            List<MavenProject> result = new ArrayList<>();

            Set<MavenKey> mavenKeys = projects.stream().map(MavenProject::getMavenKey).collect(Collectors.toSet());
            Set<String> paths = projects.stream()
                                        .map(project -> project.getProject().getFullPath().toOSString())
                                        .collect(Collectors.toSet());

            for (MavenProject project : projectToMavenProjectMap.values()) {
                boolean isAdd = false;
                for (String path : project.getModulesPath()) {
                    if (paths.contains(path)) {
                        isAdd = true;
                        break;
                    }
                }

                if (!isAdd) {
                    for (MavenArtifact artifact : project.getDependencies()) {
                        if (contains(mavenKeys, artifact.getArtifactId(), artifact.getGroupId(), artifact.getVersion())) {
                            isAdd = true;
                            break;
                        }
                    }
                }

                if (isAdd) {
                    result.add(project);
                }

            }


            return result;

        } finally {
            readLock.unlock();
        }
    }

    private boolean contains(Set<MavenKey> mavenKeys, String artifactId, String groupId, String version) {
        return mavenKeys.stream().filter(key -> Objects.equals(key.getArtifactId(), artifactId)
                                                && Objects.equals(key.getGroupId(), groupId)
                                                && Objects.equals(key.getVersion(), version))
                        .findFirst().isPresent();
    }

    public void delete(List<IProject> projects) {
        if (projects.isEmpty()) {
            return;
        }
        UpdateState state = new UpdateState();

        Deque<MavenProject> stack = new LinkedList<>();

        Set<MavenProject> childToUpdate = new HashSet<>();

        for (IProject project : projects) {
            MavenProject mavenProject = findMavenProject(project);
            if (mavenProject == null) {
                return;
            }

            childToUpdate.addAll(findChildProjects(mavenProject));
            internalDelete(findParentProject(mavenProject), mavenProject, state);
        }

        childToUpdate.removeAll(state.removedProjects);

        for (MavenProject mavenProject : childToUpdate) {
            internalUpdate(mavenProject, null, false, false, state, stack);
        }

        state.fireUpdate();
    }

    private class UpdateState {
        Map<MavenProject, MavenProjectModifications> projectWithModification = new HashMap<>();

        Set<MavenProject> removedProjects = new HashSet<>();

        public void addUpdate(MavenProject mavenProject, MavenProjectModifications modifications) {
            removedProjects.remove(mavenProject);
            projectWithModification.put(mavenProject, modifications);
        }

        public void remove(MavenProject mavenProject) {
            projectWithModification.remove(mavenProject);
            removedProjects.add(mavenProject);
        }

        public void fireUpdate() {
            if (projectWithModification.isEmpty() && removedProjects.isEmpty()) {
                return;
            }

            Map<MavenProject, MavenProjectModifications> modified = new HashMap<>(projectWithModification);
            List<MavenProject> removed = new ArrayList<>(removedProjects);
            dispatcher.projectUpdated(modified, removed);
        }
    }
}
